Dec 27, 2021
Braden Sidoti
Learn how to build a todo app with Next.js, Clerk, and Supabase. This app will add todos, sign in, sign up, user profile and multifactor authentication.
Updated: 12/2/22
In this article, we are going to explore how we can use Next.js, Clerk, and Supabase to build a very basic todo app. Thanks to Clerk’s simple and powerful authentication options we're also going to add a complete User Profile, and even Multi-factor authentication (MFA).
Why would you add MFA to a simple todo app? Because even todos deserve to be protected with the best security. And... Clerk makes it insanely easy to do so with our best-in-class Next.js authentication
We’ll cover:
Next.js is a lightweight react framework that’s optimized for developer experience, and gives you all the features you need to build powerful interactive applications.
Clerk is a powerful authentication solution specifically built for the Modern Web. Clerk lets you choose how you want your users to sign in, and makes it really easy to get started with a suite of pre-built components including <SignIn />, SignUp />, <UserButton />, and <UserProfile />. Clerk also provides complete control with intuitive SDKs and APIs.
Supabase is an open source Firebase alternative. It lets you easily create a backend with a Postgres database, storage, APIs and more. It also has an authentication module, however, for this tutorial we will only be using the database.
The quickest way to get started with Next.js is with the official create-next-app template.
In your terminal, create your app with the following command:
npx create-next-app clerk-supa
Let’s start with a simpler base by removing most of the Next.js template, and adding a basic nav bar, with a content area for our home page.
Since this is not a CSS tutorial, we'll use some very basic styling and add it all up front.
Replace the contents of styles/Home.module.css
with the following:
.header {padding: 1rem 2rem;height: 4rem;background-color: lightgray;width: 100%;display: flex;justify-content: space-between;align-items: center;}.main {margin: auto;max-width: 400px;}.label {padding: 1rem 2rem;}.container {padding: 1rem 2rem;display: flex;flex-direction: column;align-items: center;}.todoList {padding-left: 1rem;align-self: baseline;}
As promised, our basic home page will include a simple nav bar and body.
Replace the contents of pages/index.js
with the following:
import styles from "../styles/Home.module.css";export default function Home() {return (<><header className={styles.header}><div>My Todo App</div></header><main><div className={styles.container}>Sign in to create your todo list!</div></main></>)}
It's going to be beautiful... Let's run our app, and see where we're starting!
In your terminal, start your application with the following command:
npm run dev
Now you can visit http://localhost:3000
It's easiest to start an app like this with our user object in place, and with a complete authentication foundation. Then, we can build everything else on top. Clerk gives us very simple and complete building blocks that makes this whole process very easy.
First thing you'll need to do is sign up
I'm choosing the default settings, which will collect an email and password, or have users sign in with google. You can choose whatever you'd like, and once your application is created, you can fine-tune them a lot more in the Clerk dashboard.
When you create a new application, a new development instance is automatically created for you, which is optimized for local development.
Note: Instances in Clerk take a few minutes to be completely ready. Behind the scenes resources are being spun up, DNS records are being set, and certificates are being created. These take time to fully propagate across the internet, so if you see any errors, give it a minute and refresh.
Now you're going to add sign up and sign in forms, and build a "protected" page, that only signed in users can see.
You will first need to add the @clerk/nextjs
to your project. Clerk brings a ton of authentication capabilities to Next.js@clerk/nextjs
package with the following command:
Note: this tutorial will be using the latest alpha release, so make sure you're on at least version >=3.0
npm install @clerk/nextjs@next
Now you need environment variables to link your local development with your Clerk instance.
You can find your API keys on the home page, in the "Connect your application" section.
In your project’s root directory, create a file named .env.local
. Next.js will automatically load all environment variables set here, into your application. Because we're not using a custom backend at all, you only need your Frontend API key
Get the values from the dashboard, add the following to your .env.local
file:
NEXT_PUBLIC_CLERK_FRONTEND_API={your_frontend_api_key}
Note: Keys prepended with NEXT_PUBLIC are exposed to your javascript, so do not put any secret values here!
Any time you add or change your environment variables, you'll need to restart your application, so go to your terminal and kill the running application with CTRL+C or CMD+C
and relaunch it with the following command:
npm run dev
Great! although nothing changed, your application can now properly see these environment variables.
Now you can use Clerk's helper components to build a "gated" page. In order to use these helpers, you should expose the Clerk context to your entire application, by wrapping the top level component with a <ClerkProvider/>
.
In Next.js, the page exported from pages/_app.js
is the top level of your application. By wrapping the <Component />
here, every part of your application will have access to the Clerk context.
Replace pages/_app.js
with the following code:
import "../styles/globals.css";import { ClerkProvider } from "@clerk/nextjs";function ClerkSupabaseApp({ Component, pageProps }) {return (<ClerkProvider><Component {...pageProps} /></ClerkProvider>);}export default ClerkSupabaseApp;
Great! Now you can go back to pages/index.js
, add a sign in button and some basic gated content. We'll use the following Clerk-provided hooks and components:
useUser()
- hook to easily access the current user, and it's state<SignInButton />
- displays an unstyled Sign in button<SignUpButton />
- displays an unstyled Sign in button<UserButton />
- displays a "UserButton" for the current userReplace pages/index.js
with the following code:
import styles from "../styles/Home.module.css";import { useAuth, useUser, UserButton, SignInButton, SignUpButton } from "@clerk/nextjs";export default function Home() {const { isSignedIn, isLoading, user } = useUser();return (<><Header />{isLoading ? (<></>) : (<main className={styles.main}><div className={styles.container}>{isSignedIn ? (<><div className={styles.label}>Welcome {user.firstName}!</div></>) : (<div className={styles.label}>Sign in to create your todo list!</div>)}</div></main>)}</>);}const Header = () => {const { isSignedIn } = useUser();return (<header className={styles.header}><div>My Todo App</div>{isSignedIn ? (<UserButton />) : (<div><SignInButton /> <SignUpButton /></div>)}</header>);};
The header now shows either sign in/up buttons or a User Button, depending on your state. The content section of this page follows similar logic. Go ahead and sign up, if you sign up with Google, it will pull your profile image, and you will be able to see your User Button with a simple greeting.
Clerk provides a lot of hooks, components, and helpers -- you can explore all of them in our docs.
There's even more to explore with this application. The User Button has a link Manage Account, which will bring you to a complete User Profile page.
You can do a lot from this page, including:
That's right, you don't need to write any more code to add MFA to your application. Clerk handles it out of the box. Just go to the Security tab in your user profile, and follow the steps to add your phone number as a second factor!
Note: This sample app is leveraging Clerk-hosted UIs, which is the simplest implementation path. There are other ways you can add build this features, including mounting the UI
Now that your user management foundation is in place it's time to connect to a database and create some todos!
This tutorial is using Supabase as a backend. Supabase is a very powerful, open-source, postgres-based, database platform. It will be great for easily storing your todos, and even work as your API.
The first thing you'll need is a Supabase account, which can be created here
Supabase takes a little while to spin up, but once it’s ready, go to the Table editor and press the "Create a new table" button
Name the table todos and, add 2 new columns to the table named:
For each of the new columns, press the cog and deselect "Is Nullable"
Also, make sure you select "Enable Row Level Security (RLS)", which is above the Columns.
With Row Level Security enabled, postgres (and thus Supabase) will deny access to every row by default. So, you will need to setup up some "Policies" that will allow the requesting user to access their own rows, and that will let them create new rows for themselves.
With all this done, press "Save" at the bottom right of the screen.
Before creating your "RLS Policies", Supabase will need a way to figure out which user is making the current request. You normally would be able to use Supabase's built-in auth.uid()
function, however there's currently an issue that we're working to resolve
The workaround is easy enough though - you will create your own requesting_user_id()
function named that accomplishes the same thing as auth.uid()
just in a Clerk-compatible way.
All requests up to Supabase will include a JWT, which will contain the sub
claim -- which is the id of the requesting user. The following function grabs that sub
, and turns it into function so that you can use it in your "RLS Policies".
create or replace function requesting_user_id() returns text as $$ select nullif(current_setting('request.jwt.claims', true)::json->>'sub', '')::text; $$ language sql stable;
To create this function, go to the SQL Editor > New Query and copy the above into the text area and press RUN. This function is now loaded into your database and can be used as part of your RLS Policies.
You can now create two policies, one that lets your users create new todos for themself, and one that lets users retrieve all of their own todos. As a matter of practice, you only want to allow your users to do the minimum that is needed for your application.
To add an RLS policy in Supabase, go to Authentication > Policies, and press “New Policy” on the todos table.
Next, you'll want to press "Create a policy from scratch".
requesting_user_id() = user_id
Press "Review" then "Save Policy".
We’ll repeat this process for the “INSERT” operation using the same expression:
requesting_user_id() = user_id
Press "Review" then "Save Policy".
It’s almost time to jump back into code! You’ll need to set some more environment variables and install the Supabase package.
Go to your Supabase Settings > API. Here you’ll find your project's public key, URL, and JWT Secret:
You’ll need the first 2 values in your application, so add them to your .env.local
with the following format, just below the Clerk variables.
NEXT_PUBLIC_SUPABASE_KEY={your_projects_public_key}NEXT_PUBLIC_SUPABASE_URL={your_config_url}
You’ll need to add the @supabase/supabase-js
package as well.
npm install @supabase/supabase-js
You're still missing the ability to generate JWTs that Supabase can understand. To do this, you will need to sign your tokens with the JWT Secret Supabase provides (the bottom most arrow in the above screenshot).
Clerk does all this heavy lifting for you, making it easy for you to generate Supabase JWTs directly from your frontend!
Go back to your Clerk dashboard, and navigate to JWT Templates. Press "New Template" then select the "Supabase" template to get started.
This will populate a template with most of what you need. You will still need to:
Once you're done, press "Apply Changes" in the bottom right, and you're good to go! You'll now be able to call session.GetToken("Supabase")
in your frontend code.
Although this was a fair amount of setup, you've added a TON of capabilites by leveraging a Frontend Stack while barely writing any code! You now have advanced session and user management capabilities, and a connection to a powerful backend - where you can easily make authenticated requests! Time to see it in action.
First you should show a list of all of the users current todos. Go back to pages/index.js
and add a helper function that will allow you to make requests to Supabase.
// ... other importsimport { createClient } from "@supabase/supabase-js";const supabaseClient = async (supabaseAccessToken) => {const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL,process.env.NEXT_PUBLIC_SUPABASE_KEY,{global: { headers: { Authorization: `Bearer ${supabaseAccessToken}` } },});return supabase;};// ... your Home() function
Now you can make your <TodoList />
component. Add the following code to pages/index.js
:
// imports to add:import { useState, useEffect } from "react";import { useSession } from "@clerk/nextjs";// ... rest of code ...const TodoList = ({ todos, setTodos }) => {const { session } = useSession();const [loading, setLoading] = useState(true);// on first load, fetch and set todosuseEffect(() => {const loadTodos = async () => {try {setLoading(true);const supabaseAccessToken = await session.getToken({template: "supabase",});const supabase = await supabaseClient(supabaseAccessToken);const { data: todos } = await supabase.from("todos").select("*");setTodos(todos);} catch (e) {alert(e);} finally {setLoading(false);}};loadTodos();}, []);// if loading, just show basic messageif (loading) {return <div className={styles.container}>Loading...</div>;}// display all the todosreturn (<>{todos?.length > 0 ? (<div className={styles.todoList}><ol>{todos.map((todo) => (<li key={todo.id}>{todo.title}</li>))}</ol></div>) : (<div className={styles.label}>You don't have any todos!</div>)}</>);};
This code makes use of several hooks.
todos
state, but since this code still needs to access the data, its passed through as props.The logic is pretty basic for this app:
Now, update your Home()
function to display the <TodoList />
when a user is signed in:
// ... rest of code ...export default function Home() {const { isSignedIn, isLoading, user } = useUser();// Manage todos state here!const [todos, setTodos] = useState(null);return (<><Header />{isLoading ? (<></>) : (<main className={styles.main}><div className={styles.container}>{isSignedIn ? (<><span className={styles.label}>Welcome {user.firstName}!</span>// Add your TodoList here!<TodoList todos={todos} setTodos={setTodos} /></>) : (<div className={styles.label}>Sign in to create your todo list!</div>)}</div></main>)}</>);}// ... rest of code ...
Perfect. Now there's only one problem with all of this. You haven't given your users a way to create any todos, so all they will see is the following!
Now you should create a simple form so that your user can create new todos. Use the following code to create your <AddTodoForm />
component:
// ... rest of code ...function AddTodoForm({ todos, setTodos }) {const { getToken, userId } = useAuth();const [newTodo, setNewTodo] = useState("");const handleSubmit = async (e) => {e.preventDefault();if (newTodo === "") {return;}const supabaseAccessToken = await getToken({template: "supabase",});const supabase = await supabaseClient(supabaseAccessToken);const { data } = await supabase.from("todos").insert({ title: newTodo, user_id: userId }).select()setTodos([...todos, data[0]]);setNewTodo("");};return (<form onSubmit={handleSubmit}><input onChange={(e) => setNewTodo(e.target.value)} value={newTodo} /> <button>Add Todo</button></form>);}// ... rest of code ...
This components follows a similar pattern to the <TodoList />
component. It's creating a form, with an onSubmit
handler, that will send a request to Supabase to create a new todo item. Then, it updates the internal state of todos so that it can display right away.
Now you should display this component in the appropriate spot in your Home()
function:
// ... rest of code ...export default function Home() {const { isSignedIn, isLoading, user } = useUser();const [todos, setTodos] = useState(null);return (<><Header />{isLoading ? (<></>) : (<main className={styles.main}><div className={styles.container}>{isSignedIn ? (<><span className={styles.label}>Welcome {user.firstName}!</span>// Add your AddTodoForm here!<AddTodoForm todos={todos} setTodos={setTodos} /><TodoList todos={todos} setTodos={setTodos} /></>) : (<div className={styles.label}>Sign in to create your todo list!</div>)}</div></main>)}</>);}// ... rest of code ...
That's it! Now refresh your app, and try creating a new todo item, you should see it appear in your list.
There's a lot that still needs to be added to this app, even to just make it a true todo app. For starters you need a way to mark todo items complete, and delete todo items. It also needs to be productionized on it's own domain
Regardless, as you can see, NextJS, Clerk, and Supabase are truly powerful tools that let you build secure, scalable apps incredibly quickly! The Frontend Stack is shaping up to be an extremely powerful paradigm that lets you build complete applications without managing a database, or even backend code! The future of web development is shaping up to be impressive...
Thanks for reading! I hope you enjoyed this tutorial. If you have any questions join us in Discord, or reach out to me directly on twitter @bsinthewild
You can also follow us @ClerkDev to hear about the latest and greatest from Clerk.
Start completely free for up to 10,000 monthly active users and up to 100 monthly active orgs. No credit card required.
Learn more about our transparent per-user costs to estimate how much your company could save by implementing Clerk.
The latest news and updates from Clerk, sent to your inbox.