DEV Community

eugene musebe
eugene musebe

Posted on

Building a Todo App with Next.js 13, Clerk, and Supabase.

Introduction

In this guide, we will embark on a journey to build a sleek and powerful Todo application using the latest technology stack, Next.js 13, Clerk for Authentication, and Supabase as the database. With the seamless integration of Clerk and Supabase into Next.js applications, we'll discover how this combination forms a robust foundation for modern web applications, providing security, scalability, and ease of development.

Throughout this blog post, we'll delve into the step-by-step process of setting up Clerk authentication in a Next.js 13 application, ensuring secure access to our Todo app. Then, we'll explore the seamless integration of Supabase, witnessing how it effortlessly handles data storage and retrieval while providing real-time updates for a smoother user experience.

This is a continuation of the blogpost : Simplifying Authentication in Next.js Applications with Clerk hence we shall be focusing on intergrating clerk with Supabase

Let's dive in and discover the magic of building a minimalistic Todo app with Next.js 13, Clerk, and the Supabase database!

Prerequisites

Before building the Todo app, make sure you have:

  1. Basic web development skills (HTML, CSS, JavaScript).
  2. Familiarity with Next.js basics.
  3. Node.js and npm installed.
  4. A code editor.
  5. Optional: Git for version control.
  6. A Supabase account for the database.
  7. A Clerk account for authentication.

With these prerequisites, you're all set to start creating your Todo app using Next.js 13, Clerk, and Supabase. Let's begin!

Github Repository

Find the starting code on the main branch : https://github.com/musebe/Nextjs-Clerk. For the final codebase, check out the supabase branch https://github.com/musebe/Nextjs-Clerk/tree/supabase. Happy coding!

Getting Started

To get started clone the main branch from the Github repository Simplifying Authentication in Next.js Applications with Clerk. The will act as the starting point for our application as its already setup with Authentication.

To clone the applicarion run the following command :

git clone https://github.com/musebe/Nextjs-Clerk.git
Enter fullscreen mode Exit fullscreen mode

Navigate to the project directory and run the following command to install all the project dependencies :

npm install
Enter fullscreen mode Exit fullscreen mode

To start the application run the command :

npm run dev
Enter fullscreen mode Exit fullscreen mode

Supabase Setup

Supabase is an open-source Backend-as-a-Service (BaaS) platform that aims to simplify the development of web and mobile applications. It provides developers with a suite of tools and services to quickly build and scale applications without having to worry about server management or infrastructure setup.

One of the key features of Supabase is real-time updates. It leverages PostgreSQL's capabilities to provide instant and synchronized data updates to clients, making it ideal for applications that require real-time collaboration or live data streams.

To get started, follow the following steps

Step 1: Sign Up for a Supabase Account

To begin your journey with Supabase, head over to supabase.com and click on the Sign Up button located in the top right corner of the page. You can choose to sign up using your GitHub or Google account for a quick registration process. Alternatively, provide your email address and fill in the necessary details to create an account.

Step 2: Create a New Project

Once you have successfully signed up and logged in, you will be directed to your Supabase dashboard. Here, click on the New Project button to create a new project. Give your project a meaningful name and select the preferred region for data storage. Supabase offers multiple regions to ensure low-latency access to your data as highlighted below :

Supabase setup

Step 3: Set Up Your Database

After creating your project, you will land on the project overview page. From the left sidebar, select the SQL Editor tab. To set up a new database, click on "Create table" template and run the following query.

CREATE TABLE todos (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  user_id VARCHAR,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, NOW()) NOT NULL,
  completed_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, NOW()) NOT NULL,
  todo VARCHAR,
  tag VARCHAR
);

Enter fullscreen mode Exit fullscreen mode

Todos Query

The Supabase query above creates a table named "todos" with the following columns:

  1. id (bigint): This column is an auto-generated primary key of type bigint. It will automatically assign a unique identifier to each row added to the table.

  2. user_id (varchar): This column stores the user identifier associated with the todo. It is of type varchar, which means it can hold alphanumeric characters and symbols.

  3. created_at (timestamp with time zone): This column represents the timestamp when the todo was created. It has a default value set to the current time in Coordinated Universal Time (UTC). The now() function retrieves the current timestamp, and the timezone('utc'::text, ...) function ensures it is in UTC format.

  4. completed_at (timestamp with time zone): This column represents the timestamp when the todo was marked as completed. Similar to created_at, it has a default value set to the current time in UTC.

  5. todo (varchar): This column stores the actual todo item, such as a task or reminder. It is of type varchar.

  6. tag (varchar): This column allows categorizing or labeling the todo item with a tag. It is also of type varchar.

After running the query above click on the table editor tab on the left menu and you will see a todos table created with the structure below :

Todos Table

Configure RLS (Row Level Security) for thetodos table

Configuring Row Level Security (RLS) in Supabase provides an essential layer of data protection and access control for your database tables. RLS allows you to define rules that determine which rows of data a user can access based on their specific attributes or roles. This feature plays a crucial role in enhancing the security and privacy of your application's data.

To create our first row level security run the following command on the SQL Editor

create or replace function requesting_user_id() returns text as $$
  select nullif(current_setting('request.jwt.claims', true)::json->>'sub', '')::text;
$$ language sql stable;

Enter fullscreen mode Exit fullscreen mode

policy_query

The above is a function named requesting_user_id(). This function returns the user ID (sub claim) extracted from the JSON web token (JWT) included in the request header.

Next we need to create a policy that allows only authenticated users to update their own todos. Run the following query as highlighted below :

CREATE POLICY "Authenticated users can update their own todos"
ON public.todos FOR UPDATE
USING (
    auth.role() = 'authenticated'::text
)
WITH CHECK (
    requesting_user_id() = user_id
);

Enter fullscreen mode Exit fullscreen mode

Authenticate

This code creates a policy named "Authenticated users can update their own todos" on the public.todos table. The policy allows updates to the table only if the user has the role "authenticated" and the requesting user ID matches the user_id column in the table.

To check if the above policies have been applied to our todos database, navigate to the Authentication tab and click the policies button. you should be able to see this :

Policies

To finalize the database setup with Supabase, you will need to obtain the Supabase API Key and URL. These credentials are essential for connecting your application to the Supabase database and making authenticated requests to access data or perform CRUD operations.

These credentials serve as your project's authentication tokens. They allow you to interact with the Supabase API securely.

  • API Key: The API Key acts as your secret key to authenticate requests to the Supabase API. It is a long string of characters and numbers, used to identify and authorize your application.

  • URL: The URL is the endpoint through which you will make requests to the Supabase API. It typically follows the format https://your-project-id.supabase.co, where your-project-id is replaced with your specific Supabase project ID.

To view your keys click on the Settings tab. This will take you to the project settings where all the keys are stored. Click on the API tab to revel your keys as shown below :

Keys

Copy the keys and paste them in the .env file of your project as shown below :

NEXT_PUBLIC_SUPABASE_URL= YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_KEY=YOUR_SUPABASE_ANON_PUBLIC_KEY
Enter fullscreen mode Exit fullscreen mode

Supabase requires JWTs be signed with the HS256 signing algorithm and use their signing key. Find the JWT secret key in your Supabase project under Settings > API in the Config section. Copy the JWT secret

jwt token

To complete the integration between Supabase and the Clerk authentication service, follow these steps:

  1. Access your Clerk dashboard: Log in to your Clerk account and navigate to the dashboard.

  2. Go to JWT Templates: In the Clerk dashboard, locate the JWT Templates page and click on it. This page allows you to configure JSON Web Token (JWT) templates for different authentication providers.

  3. Create a new template: On the JWT Templates page, click the button to create a new template. Select "Supabase" as the authentication provider for which you want to configure the template.

  4. Set the Signing Key: In the template configuration, find the field labeled "Signing Key" and paste the "JWT secret" key that you previously copied from your Supabase project. This key is essential for securely signing and verifying the JWTs issued by Clerk.

By following these steps and configuring the JWT template in the Clerk dashboard, you have successfully integrated Supabase with Clerk's authentication service. Clerk will now be able to generate and validate JWTs using the Supabase signing key, providing secure and seamless authentication for your application as highlighted below :

JWT

Installing Supabase

Congratulations! With the database set up and Clerk configured with Supabase, you are now ready to integrate Supabase into your Next.js project. To do so, follow these simple steps:

  1. Open your Next.js project: Navigate to the root directory of your Next.js project in your terminal or code editor.

  2. Install the Supabase client library: Run the following command to install the official Supabase client library for JavaScript:

npm install @supabase/supabase-js
Enter fullscreen mode Exit fullscreen mode

This command will fetch and install the required @supabase/supabase-js package from the npm registry and add it to your project's dependencies.

Centralizing Supabase Client

To enhance organization and reusability in your Next.js project, we can centralize the creation and configuration of the Supabase client by creating a utils folder at the root of the project structure. Within this folder, we'll add a file named supabaseClient.js. This approach helps maintain clean code and facilitates easier management of the Supabase client across multiple components and pages.

Here's how you can achieve it:

Step 1: Create the utils folder

In your Next.js project, navigate to the root directory and create a new folder named utils.

Step 2: Add supabaseClient.js to the utils folder

Within the utils folder, create a new file named supabaseClient.js.

Step 3: Place the Supabase Client Configuration Code

In the supabaseClient.js file, add the following code to create and configure the Supabase client:

import { createClient } from '@supabase/supabase-js';

export const supabaseClient = async (supabaseToken) => {
  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL,
    process.env.NEXT_PUBLIC_SUPABASE_KEY,
    {
      global: { headers: { Authorization: `Bearer ${supabaseToken}` } },
    }
  );

  return supabase;
};

Enter fullscreen mode Exit fullscreen mode

The purpose of this code snippet is to create a Supabase client, which is a client-side library responsible for communication between the frontend application and the Supabase backend. This client enables seamless interaction with the database, making it easier to perform various operations such as querying data, managing real-time updates, and handling authentication.

The supabaseClient function, which is exported from this code snippet, accepts a supabaseToken as an input parameter. This token is typically an authentication token obtained from Supabase, uniquely associated with a user.

Inside the function, the createClient function from the @supabase/supabase-js library is utilized to instantiate the Supabase client. This function requires three arguments: process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_KEY, and an object containing additional configuration options.

The first argument, process.env.NEXT_PUBLIC_SUPABASE_URL, represents the URL of the Supabase project. It is usually stored as an environment variable and serves as the endpoint for server communication.

The second argument, process.env.NEXT_PUBLIC_SUPABASE_KEY, represents the public API key of the Supabase project. Like the URL, this is stored as an environment variable and provides the necessary authentication and authorization for the client.

The third argument is an object that specifies further configuration options for the Supabase client. In this code snippet, it sets the Authorization header for each request using the provided supabaseToken. This ensures that the client is authenticated and authorized to access the relevant resources on the Supabase backend.

Finally, the supabaseClient function returns the configured Supabase client instance, which can then be used across the application to interact with the database and handle real-time updates. By encapsulating the client creation and configuration within this utility, it promotes code reusability and maintains a clean structure within the project.

Performing Requests to Fetch and Post Todos from Supabase: Creating requests.js in the utils Folder

To handle fetching and posting todos to Supabase, we can create a file named requests.js within the utils folder. This file will contain functions that encapsulate the logic for making API requests to the Supabase database. By doing so, we can keep the API interaction centralized and easily manage todo-related requests in our Next.js project.

Let's go ahead and create the requests.js file:

Step 1: Create requests.js in the utils folder

In your Next.js project, navigate to the utils folder, which you created earlier, and create a new file named requests.js.

Step 2: Implement Fetch and Post Functions

In the requests.js file, add the necessary functions to fetch and post todos to Supabase. Below is an example of how you can implement these functions:

Fetch Todos

// utils/requests.js
import { supabaseClient } from './supabaseClient';

// Function to fetch todos from Supabase
export const getTodos = async ({ userId, token }) => {
  const supabase = await supabaseClient(token);
  const { data: todos, error } = await supabase
    .from("todos")
    .select("*")
    .eq("user_id", userId);

  if (error) {
    console.error('Error fetching todos:', error.message);
    return [];
  }

  return todos;
};
Enter fullscreen mode Exit fullscreen mode

The getTodos function is an asynchronous function that accepts an object with userId and token properties as parameters. It first awaits the supabaseClient function (imported from './supabaseClient') passing the token as an argument. This function creates and returns the Supabase client. Using the Supabase client, it performs a select query on the "todos" table, filtering the results based on the user_id column that matches the userId parameter.
The result is destructured to extract the data and error properties. If an error occurs during the fetch operation, it logs an error message to the console and returns an empty array ([]).
Otherwise, it returns the fetched todos data.

Post Todos

export const postTodo = async ({ userId, token, e }) => {
  const supabase = await supabaseClient(token);
  const { data, error } = await supabase
    .from('todos')
    .insert({
      user_id: userId,
      todo: e.target[0].value,
      tag: e.target[1].value,
    })
    .select();

  if (error) {
    console.error('Error posting todo:', error.message);
    return null;
  }

  return data;
};

Enter fullscreen mode Exit fullscreen mode

The postTodo function is also an asynchronous function that accepts an object with userId, token, and e properties as parameters.
It awaits the supabaseClient function (imported from './supabaseClient') passing the token as an argument to create the Supabase client.
Using the Supabase client, it performs an insert operation on the "todos" table, inserting a new row with values for user_id, todo, and tag columns obtained from the e parameter (which is expected to contain event-related information).
The result is destructured to extract the data and error properties.
If an error occurs during the insert operation, it logs an error message to the console and returns null.
Otherwise, it returns the inserted data.

Now that you have created the fetchTodos and postTodo functions in the requests.js file, you can use them in your components or pages to interact with your Supabase database. Import the functions where needed and call them to fetch or post todos.

Post Todos Component

To post todos to Supabase, create a create-todo folder inside the app directory, then add a page.jsx file inside it with the following code:

import { useState } from 'react';
import { useRouter } from 'next/router';
import { postTodo } from '../../utils/requests';
import { useAuth } from '@clerk/nextjs';
import Form from '@components/Form';

const CreateToDo = () => {
  const router = useRouter();
  const { userId, getToken } = useAuth();

  const [submitting, setSubmitting] = useState(false);
  const [formData, setFormData] = useState({ todo: '', tag: '' });

  const createTodo = async (e) => {
    e.preventDefault();
    try {
      setSubmitting(true);
      const token = await getToken({ template: 'supabase' });
      const posts = await postTodo({ e, userId, token });
      setFormData(posts);
      if (posts) {
        router.push('/');
      }
    } catch (error) {
      console.error('An error occurred:', error);
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <Form
      type='Post'
      post={formData}
      setPost={setFormData}
      submitting={submitting}
      handleSubmit={createTodo}
    />
  );
};

export default CreateToDo;

Enter fullscreen mode Exit fullscreen mode

The CreateToDo component is responsible for handling the creation of todos and posting them to the Supabase backend.

It imports the required dependencies, including useState for managing component state, useRouter from Next.js to access the router functionality, postTodo from the utils/requests module to send the todo data to the server, useAuth from @clerk/nextjs for authentication using Clerk, and Form from @components/Form for rendering the form.

The component uses the useState hook to manage the state variables submitting and formData. submitting is used to indicate whether the form is currently being submitted, while formData holds the todo and tag values from the form.

The createTodo function is defined to handle the form submission. It is an asynchronous function to handle the asynchronous tasks such as getting the authentication token and posting the todo data to the server.

When the form is submitted (e.preventDefault() is called), the function sets submitting to true to indicate that the form submission is in progress.

It then uses useAuth to obtain the user's ID and gets the Supabase authentication token using getToken({ template: 'supabase' }).

The postTodo function is called with the necessary data, including the event (e), the user's ID, and the authentication token, to post the todo data to Supabase.
The response from the server is stored in the posts variable, and the state is updated with the response data using setFormData.

If the post was successful (if (posts)), the user is redirected to the homepage using the Next.js router (router.push('/')).

Any errors that occur during the process are caught in the catch block and logged to the console for debugging purposes.

Regardless of success or failure, setSubmitting(false) is called in the finally block to set submitting back to false after the form submission process is completed.

Finally, the Form component is rendered with the appropriate props, including the type, post, setPost, submitting, and handleSubmit. These props are used to pass the necessary information and functions to the Form component to display and handle the form elements.

Retrieve Todos Component

To retrieve the posted todos from Supabase and create a Feed.jsx component to display them, follow these steps:

  1. Import the necessary dependencies:
'use client';
import { useState, useEffect } from 'react';
import  {  getTodos  }  from  '../utils/requests';
Enter fullscreen mode Exit fullscreen mode

Create the Feed component:

const Feed = ({ userId, token }) => {
  const [todos, setTodos] = useState([]);

  useEffect(() => {
    const fetchTodos = async () => {
      try {
        const todosData = await getTodos({ userId, token });
        setTodos(todosData);
      } catch (error) {
        console.error('Error fetching todos:', error.message);
        setTodos([]);
      }
    };

    fetchTodos();
  }, [userId, token]);

  return (
    <div>
      <h2>Todo Feed</h2>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default Feed;
Enter fullscreen mode Exit fullscreen mode

In the Feed component above, the useState hook is used to create a state variable todos and the setTodos function to update it. The initial value of todos is an empty array.

The useEffect hook is used to fetch the todos when the component is mounted. It calls the fetchTodos function asynchronously, which is defined inside the useEffect hook.

Inside the fetchTodos function, the getTodos function is called with the provided userId and token. The returned todos data is stored in the todosData variable and then set as the new value of the todos state using setTodos(todosData).

If an error occurs during the API call, it is caught in the catch block, logged to the console, and the todos state is set to an empty array to clear any previously fetched data.

The rendered JSX in the return statement displays the list of todos. Each todo is mapped to a <li> element with a unique key attribute set to todo.id, and the todo.title is displayed as the content of the list item.

Finally, the Feed component is exported as the default export.

Conclusion

In conclusion, we successfully built a sleek Todo app using Next.js 13, Clerk for Authentication, and Supabase as the database. Clerk ensured secure access, while Supabase handled data storage and real-time updates. The integration of these technologies formed a powerful foundation for modern web apps with enhanced security and user experience. Happy coding!

Your final application should looki like this

Clerk-Supabase

Reference

For further guidance and information, refer to the following:

  1. Next.js Documentation: Access the official Next.js documentation at https://nextjs.org/docs for in-depth details about the framework.

  2. Clerk Documentation: Explore Clerk's authentication capabilities and integration guides at https://clerk.com/docs to understand its usage better.

  3. Supabase Documentation: Learn about Supabase's real-time database features and integration options at https://supabase.com/docs.

  4. Simplifying Authentication in Next.js Applications with Clerk: Find detailed steps for setting up Clerk authentication in Next.js apps in the blog post: Dev.to Link.

These references will aid you in building the Todo app with Next.js 13, Clerk, and Supabase effectively. Keep them handy for quick access to valuable information during your development journey.

Top comments (8)

Collapse
 
mastoj profile image
Tomas Jansson

Great writeup! Short question, how do you get access to the token if on the server?

Collapse
 
musebe profile image
eugene musebe

Clerk offers SDKs for various backend languages, including Node.js for server-side auth. Check out this link for more details: Nodejs Docs. Depending on your backend language, you can browse their documentation to find the specific SDK tailored for your needs.

Collapse
 
reevotek profile image
reevotek

Hello Eugene,

Great and detailed tutorial.
Although I learned a lot, I was not able to create todos.
I clone the repo and follow the tutorial to the T, but to no avail.

Also, the images are really low definition. I'd be great if you can update them.

Best regards,
Jorge

Collapse
 
musebe profile image
eugene musebe

Hello Reevotek,

Would you mind cross-checking your codebase with the final repo: github.com/musebe/Nextjs-Clerk/tre....

Collapse
 
reevotek profile image
reevotek

Hi Eugene,

I compared my code with yours and a couple of things where missing.

  1. When I cloned your repo, the utils folder was not included. It was included when I downloaded with the zip folder, though. maybe I had something else cached on my clipboard.

  2. The tutorial is missing the TodoCard.jsx component, as well as some other functions inside the Feed.jsx component.

  3. And last, I think I might have an error on my Supabase DB. I had a hard time seeing the images on your tutorial.

The app is still not working. I'll give it another try in a day or so and report back to you.

Best regards,
Jorge

Collapse
 
reevotek profile image
reevotek

Hello Eugene,
Will do.
Thanks for replying.

Regards,
Jorge

Collapse
 
emmadjams profile image
EMMADJAMS

bro why couldn't you do something simple like retrieving data from clerk into supabase

Collapse
 
adamcw profile image
Adam W • Edited

@musebe nice! I wonder how you would have written the table's INSERT RLS rule? Because the new row getting created can't be validated via clerk user id. Any idea?